#! /usr/bin/env node
const { program } = require('commander');
const fs = require('fs');
const path = require('path');
const { exit } = require('process');
const urlp = require('url');

// todo: url 包含 '797779122:1689927801:HLAgYovaXLpp0ol_TbcGFDvEuOK9oA2bKtDWCLrxE4Y' 不能创建文件夹

program
  .name('mirror-site')
  .option('-u, --url <string>', 'URL of the site to be mirrored')
  .option('-m, --match <string>', 'The regular expression of the URL that needs to be mirrored within the site, and URLs that do not match the path part of the expression will be ignored. This is very useful for filtering multilingual URLs containing language information (such as /en/index.html) to avoid duplicate collection of the same page. If you only want to collect English, enter "-m \\/en.*"')
  .option('-o, --output <string>', 'Output directory (default to the directory at command line execution + site domain)')
  .option('-p, --port <ushort>', 'Use a Chrome browser that has been opened for remote debugging for mirroring operation (command: "C:\Program Files (x86)\Google\Chrome\Application\chrome.exe" --remote-debugging-port=9222), this parameter is the remote debugging port number 9222')
  .option('-new --newPage', 'When using `-p`, priority will be given to finding the tab of the site that has already been opened in your browser. If the program gets stuck in "Configuring mirror operations...", Unable to operate your tab for unknown reasons. Use `-new` to open a new tab for operation')
  .option('-remote --remote', 'Do you want to download external resources (excluding HTML web pages)')
  .option('-t, --types <string>', 'The data type to be mirrored. If left blank, it defaults to mirroring the website. Optional xhr(json) | document(html) | stylesheet(CSS) | script(js) | image | media(audio/video) | font, multiple separated by "|", such as image|media')
  .option('-load --load', "Continue from the location of the last mirror record. Every URL completed will automatically save the record, which will be recorded in the root directory of the output folder/mirror-sites-record.json")
  .option('-source --source', "The downloaded HTML maintains the source code and does not replace the full URL address with a relative address. It is useful for situations where frameworks such as vue.js are used and the source code contains custom components")

if (process.argv.length === 2) {
  program.outputHelp();
  return;
}

program.parse();

let { url, match, output, port, remote, types, load, source, newPage } = program.opts();
const protocols = ['http:', 'https:']
const status = [200, 304]
const UrlType = {
  "-1": "External",
  "0": "Waiting",
  "1": "Completed",
  "2": "Failed",
  "3": "Ignore",
}

console.log("mirror-site:", url)
const location = urlp.parse(url, true);
if (!output)
  output = __dirname + '/' + location.host + '/';
if (!output.endsWith('/'))
  output += '/';
if (!fs.existsSync(output))
  fs.mkdirSync(output, { recursive: true });
// 自动跳转链接镜像站点
let auto = true;
// 正则表达式
const regex = match ? new RegExp(match) : undefined;
// 存档
const record = output + "mirror-sites-record.json";
/** 存档
 * @typedef {object} save
 * @property {Record<string, number>} failed - 加载失败的 url，避免耗时重复加载
 * @property {Record<string, number>} download - 加载成功的 url，避免耗时重复处理并写入文件
 * @property {Record<string, number>} links - 嗅探到的网站包含的所有链接(内链和外链)，用于爬虫爬行以镜像整个网站的内容
 */
/** @type {save} */
const save = load && fs.existsSync(record) ? JSON.parse(fs.readFileSync(record))
  : {
    failed: {},
    links: (() => {
      const t = {};
      t[url] = 0;
      return t;
    })(),
    download: {},
  };

// todo: 或许有不需要去掉参数的情况，有可能出现死循环，但也可以采集更多不同的数据，就看是采集数据为主还是镜像站点为主
/** 获取去掉参数的 url
 * @param {string} url - 原始 url
 * @returns {string} - 去掉参数的 url
 */
function getUrlWithoutParam(url) {
  const url2 = urlp.parse(url, true);
  return `${url2.protocol}//${url2.host}${url2.pathname}`;
}
/** 将一个 url 的资源写入文件
 * @param {urlp.UrlWithParsedQuery} url - 解析过的 url
 * @param {"document" | "stylesheet" | "image" | "media" | "font" | "script" | "texttrack" | "xhr" | "fetch" | "prefetch" | "eventsource" | "websocket" | "manifest" | "signedexchange" | "ping" | "cspviolationreport" | "preflight" | "other"} type - 资源类型
 * @param {string | NodeJS.ArrayBufferView} data - 要写入的数据
 */
function writeFile(url, type, data) {
  // 采集资源：分类存放
  // 镜像站点：站点放根目录
  // 下载到相应文件夹下
  /** @type {string} */
  let file = output;
  if (types) {
    file += type;
  } else {
    if (url.host != location.host)
      file += url.host;
    else
      // 最后多了一个 '/' ，不过两个 '//' 貌似也没什么问题
      file = file.substring(0, file.length - 1);
  }
  file += url.pathname == '/' ? '/index.html' : url.pathname;
  // 当请求没有后缀(接口)，有可能有和文件夹重名导致无法写入
  if (!path.extname(file) && (type == 'document' || fs.existsSync(file)))
    file += '.html';
  const directory = path.dirname(file);
  if (!fs.existsSync(directory))
    fs.mkdirSync(directory, { recursive: true });
  fs.writeFileSync(file, data);
}
/** 获取/设置链接的值，http 和 https 认为是一样的链接
   * @param {string} href - 链接
   * @param {number} value - 不为undefined则设置值，0且还没有链接则自动设置合适的值
   * @returns {number} - 1: 已完成 / 0: 未完成 / -1: 外链 / 3: 忽略 / undefined: 没有链接
   */
function linkValue(href, value) {
  if (save.links[href] != undefined) {
    if (value != undefined)
      save.links[href] = value;
    return save.links[href];
  }
  const href2 = href.startsWith("http://") ?
    'https' + href.substring(4) :
    'http' + href.substring(5);
  if (save.links[href2] != undefined) {
    if (value != undefined)
      save.links[href2] = value;
    return save.links[href2];
  }
  if (value != undefined) {
    if (value == 0) {
      const url = urlp.parse(href, true);
      if (url.host != location.host)
        value = -1;
      // 正则表达式限定地址
      else if (regex && !regex.test(url.path))
        value = 3;
      else
        console.log("Append site url:", href)
    }
    save.links[href] = value;
    return value;
  }
  return undefined;
}
/** 打印当前下载结果 */
function information() {
  const links = save.links;
  const urls = {};
  for (const link in links) {
    const type = links[link];
    if (!urls[type])
      urls[type] = [];
    urls[type].push(link);
  }
  for (const type in urls) {
    console.log(`\n${UrlType[type]}[${urls[type].length}]:`)
    console.log(urls[type]);
  }
  console.log(`\nDownload success[${Object.keys(save.download).length}]:`);
  console.log(Object.keys(save.download));
  console.log(`\nDownload failed[${Object.keys(save.failed).length}]:`);
  console.log(Object.keys(save.failed));
}

(async function () {
  let puppeteer = require('puppeteer-core');
  let browser;
  let browserWSEndpoint;
  if (port) {
    console.log("Trying get chrome webSocketDebuggerUrl with port", port)
    browserWSEndpoint = await new Promise(resolve => {
      const url = "http://127.0.0.1:" + port + "/json/version";
      fetch(url).then((ret) => {
        ret.json().then(i => {
          resolve(i.webSocketDebuggerUrl);
        })
      }).catch(err => {
        console.log("Can not connect chrome.", url, err);
        resolve(undefined);
      })
    })
  }
  if (browserWSEndpoint) {
    console.log("Connecting chrome", browserWSEndpoint)
    browser = await puppeteer.connect({ browserWSEndpoint, defaultViewport: null });
    // 不需要关闭chrome
    browser.close = () => { };
    console.log("Connect chrome success!");
  } else {
    console.log("Opening chromium!");
    puppeteer = require('puppeteer');
    browser = await puppeteer.launch({
      headless: false,
      defaultViewport: null
    });
    console.log("Open chromium success!");
  }

  console.log("Configuring mirroring operations...")
  /** @type {puppeteer.Page} */
  let page;
  if (port && !newPage)
    page = (await browser.pages()).find(p => p.url().startsWith(url) || url.startsWith(p.url()));
  if (!page)
    page = await browser.newPage();
  // page 被关闭时推出程序
  page.on("close", () => exit());
  console.log("- Page opened!")

  await page.setCacheEnabled(false);
  // 对于镜像网站，如果有安全验证等操作的，希望直到 url 指定的第一个请求完成后才开始镜像
  let connected = !types ? false : true;
  await page.setRequestInterception(true);
  /**
   * @typedef {object} request
   * @property {puppeteer.HTTPRequest} request - 请求
   * @property {number} urlID - 请求发出时的 page 状态，切换 url 或刷新页面后，请求将失效，失效的请求即便失败也不能视为失败
   * @property {number} time - 请求发出的时间
   * @property {'pending' | 'success' | 'failed'} status - 请求的完成状态
   */
  /** @type {Record<string, request>} */
  let requests = {};
  /** 等待网络资源全部加载完成
   * @param {function():boolean} completed - 其它控制请求完成状态的方法
   */
  function waitRequestAllCompleted(completed) {
    return new Promise(resolve => {
      /** @type {request[]} */
      let previous = [];
      // 等待网络加载时一段时间显示还有哪些网络请求没有完成
      function waiting() {
        if (completed?.())
          return;
        /** @type {request[]} */
        const incomplete = [];
        for (const id in requests) {
          const r = requests[id];
          if (r.urlID != urlID)
            continue;
          if (r.status == 'pending')
            incomplete.push(r);
        }
        // 等待加载的资源发生变化则输出日志
        if (incomplete.length) {
          // if (previous.length != incomplete.length || previous.some(i => !incomplete.includes(i))) {
          console.log("Waiting for network idle...")
          console.log(incomplete.map(r => `pending ${((Date.now() - r.time) / 1000).toFixed(3)}s: ${r.request.url()}`));
          // }
        }
        previous = incomplete;
        if (!completed && incomplete.length == 0)
          resolve();
        else
          setTimeout(waiting, 3000);
      }
      // !completed 留下的也是下载完资源后给 vue 进行数据渲染的时间
      setTimeout(waiting, completed ? 6000 : 1000);
    })
  }
  /** 获得或创建一个 request
   * @param {puppeteer.HTTPRequest} r - 请求
   * @param {'pending' | 'success' | 'failed'} status - 创建时的状态
   * @returns {request}
   */
  function getOrDelete(r, status) {
    let request = requests[r._requestId];
    if (request) {
      if (request.status == 'pending' && status != 'pending')
        request.status = status;
      delete requests[r._requestId];
    } else {
      request = {
        request: r,
        urlID: urlID,
        status: status,
        time: Date.now(),
      };
      requests[r._requestId] = request;
    }
    return request;
  }
  /** 请求完成的处理
   * @param {puppeteer.HTTPRequest} r - 完成的请求
   */
  async function requestSuccess(r) {
    const res = r.response();
    const request = getOrDelete(r, 'success');
    const headers = res.headers();

    // 被重定向的网页视为失败
    if (linkValue(r.url()) == 0 && res.status() == 302 || res.status() == 301) {
      save.links[r.url()] = 2;
      const redirect = headers['location'];
      console.log("URL redirected! It may be an interface URL:", r.url(), "->", redirect);
      if (redirect && linkValue(redirect) == null)
        // 将重定向的 url 加入请求中去
        linkValue(redirect, 0);
      return;
    }

    const url2 = getUrlWithoutParam(r.url());
    if (save.download[url2])
      return;

    let rt = res.request().resourceType();
    switch (rt) {
      case 'xhr':
      case 'fetch':
        // 也可能有有用的数据类型
        const ct = headers['content-type'];
        if (ct) {
          if (ct.includes('text/html'))
            rt = 'document';
          else if (ct.includes('image/'))
            rt = 'image';
          else if (ct.includes('application/javascript'))
            rt = 'script';
          else if (ct.includes('audio') || ct.includes('video'))
            rt = 'media';
          else if (ct.includes('font'))
            rt = 'font';
          else if (ct.includes('css') || ct.includes('less') || ct.includes('sass'))
            rt = 'stylesheet';
        }
        break;
    }

    switch (rt) {
      case 'xhr':
      case 'fetch':
      case 'document':
      case 'stylesheet':
      case 'image':
      case 'media':
      case 'font':
      case 'script':
      // 像 .ico | .pdf | .zip 等特殊资源
      case 'other':
        const rurl = urlp.parse(r.url(), true);
        // 还有类似于 data:image & blob:https 等特殊的协议方案
        if (!protocols.includes(rurl.protocol)) {
          // console.log(`skip special protocol '${rurl.protocol}' `, r.url());
          return;
        }
        // 跳过 200 以外的返回
        if (!status.includes(res.status())) {
          // console.log(`skip response status '${r.status()}' `, r.url());
          return;
        }
        // 跳过预检请求 (OPTIONS)
        if ((res.request().method == 'OPTIONS' && headers['access-control-allow-methods'])
          || res.request().initiator()?.type == 'preflight') {
          console.log(`skip preflight request`, r.url());
          return;
        }
        // 检查需要下载的文件类型
        if (!types || types.includes(rt)) {
          if (!connected) {
            connected = r.url() == url || r.url() == (url + '/');
            if (connected)
              console.log("Officially starting to mirror websites!", url);
            else
              return;
          }
          // 站内的外链资源需要 remote 才能打开
          if (rurl.host != location.host && !remote) {
            // console.log(`skip external resource`, r.url());
            return;
          }
          // todo: 收到了 response ，但是没下载完(网速慢)，链接就跳了，就会有如下报错
          // Could not load body for this request. This might happen if the request is a preflight request.
          // 暂时没解决时可以记录到存档里后面可以手动下载，否则会真的没下载成功这个资源
          // 下载资源文件
          await res.buffer().then(i => {
            writeFile(rurl, rt, i)
            console.log(`Download success! [${((Date.now() - request.time) / 1000).toFixed(3)}]s`, r.url())
            save.download[url2] = 1;
          }).catch(i => {
            request.status = 'failed';
            console.error("get response buffer error!", r.url(), i, res.headers(), res.status(), res.statusText(), res.request())
          })
        }
        break;

      // default:
      //   console.log("skip other type request", rt, r.url())
      //   break;
    }
  }
  page.on("request", async (r) => {
    getOrDelete(r, 'pending');
    // console.log("request", r, r.url())
    if (r.isInterceptResolutionHandled()) return;
    if (save.failed[getUrlWithoutParam(r.url())]) {
      r.abort();
    } else {
      r.continue();
    }
  })
  page.on("requestfinished", async (r) => {
    // console.log("requestfinished", r)
  })
  page.on("requestfailed", async (r) => {
    const request = getOrDelete(r, 'failed');
    // 下载时间很短就失败了也无所谓(需要多尝试几次)
    if (Date.now() - request.time < 2000)
      return;
    // todo: post 发送的 url 可能都一样，但是 postData 不一样，所以这里可能不能使用 getUrlWithoutParam
    const url = getUrlWithoutParam(r.url());
    // 成功了，后来又失败了，不算失败
    if (save.download[url])
      return;
    if (r.resourceType() != 'document' && !save.failed[url] && !save.links[url]) {
      console.log(`Request failed!  [${((Date.now() - request.time) / 1000).toFixed(3)}]s`, r.url())
      save.failed[url] = 1;
    }
  })
  page.on("requestservedfromcache", async (r) => {
    // console.log("Request served from cache!", r.url(), r)
  })
  page.on("response", async (res) => {
    const r = res.request();
    // console.log("response", r.url())
    await requestSuccess(r);
  })
  console.log("- Configuration completed!")
  /** 自动走链接 */
  async function automatic() {
    const links = save.links;
    page.off('framenavigated', urlchanged);
    page.once('framenavigated', urlchanged);
    for (const link in links) {
      if (links[link] == 0) {
        console.log("Auto mirroring:", link);
        let trycount = 3;
        while (trycount > 0) {
          let success = true;
          await page.goto(link, {
            referer: page.url(),
            timeout: 0,
            // "load" | "domcontentloaded" | "networkidle0" | "networkidle2"
            waitUntil: "domcontentloaded",
          }).catch(ex => {
            console.log("goto", link, "failed!", ex)
            success = false;
          });
          if (success || links[link] == 1)
            break;
          console.log("Retry mirroring url:", link)
          // 稍微等待，免得切换页面太过于频繁
          await new Promise(r => {
            setTimeout(() => r(0), 2000)
          })
          trycount--;
          if (trycount <= 0) {
            links[link] = 2;
          }
        }
        return;
      }
    }
    console.log("Mirror site completed!", url);
    // todo: 完成时将下载失败的 url 再重新尝试下载一遍，查漏补缺
    information();
    exit();
  }
  let urlID = 1;
  // page.on('framenavigated', urlchanged);
  /** URL 改变开始新页面的采集时 */
  async function urlchanged(frame) {
    if (frame != page.mainFrame())
      return;
    const currentID = urlID;
    urlID++;
    const currentURL = page.url();
    console.log("Mirroring:", currentURL);
    const links = save.links;
    if (linkValue(currentURL) == 0) {
      console.log("Waiting for network idle...")
      // await new Promise(r => { setTimeout(() => r(), 1000) })
      // 等待资源加载完，document.readyState == 'complete'
      let completed = false;
      while (!completed) {
        await Promise.race([
          new Promise(r => { setTimeout(() => r(), 60000) }), // 最多等 60s
          page.waitForFunction(() => document.readyState == 'complete'),
          waitRequestAllCompleted(() => completed)])
          .then(i => completed = true).catch(i => { })
      }
      // 下载完资源后，vue 页面可能会对数据进行渲染，进而产生新的网络请求
      await Promise.race([
        new Promise(r => { setTimeout(() => r(), 60000) }), // 最多等 60s
        waitRequestAllCompleted()])
        .then(i => { }).catch(i => { })
      console.log("Network loading completed", currentURL)
      // 找页面里的所有链接
      console.log("Sniffing internal links")
      /**
       * @typedef {object} html
       * @property {string} url
       * @property {string} html
       * @property {Record<string, undefined>} hrefs - 站内链接
       */
      const html = await page.evaluate(
        /** 像爬虫一样通过 a 标签找出所有网站内的链接以用于后续的爬行
         * @param {'none' | 'local' | 'all'} replace - 网页引用资源的 url 全路径替换成相对路径
         * 
         * - none: 不替换
         * 
         * - local: 替换本站的引用
         * 
         * - all: 包含外站的引用，例如引用 cdn 的资源
         * 
         * @returns {html}
         */
        (replace) => {
          // a 标签可以跳转的站内链接
          const hrefs = {};
          // 是否有改写全路径为相对路径
          let rewrite = false;
          // HTML标准标签列表
          const validHTMLTags = [
            "a", "abbr", "acronym", "address", "animate", "animateMotion", "animateTransform", "applet", "area",
            "article", "aside", "audio", "b", "base", "basefont", "bdi", "bdo", "big", "blink", "blockquote", "body",
            "br", "button", "canvas", "caption", "center", "circle", "cite", "clipPath", "code", "col", "colgroup",
            "color-profile", "data", "datalist", "dd", "defs", "del", "desc", "details", "dfn", "dialog", "dir", "div",
            "dl", "dt", "ellipse", "em", "embed", "feBlend", "feColorMatrix", "feComponentTransfer", "feComposite",
            "feConvolveMatrix", "feDiffuseLighting", "feDisplacementMap", "feDistantLight", "feDropShadow",
            "feFlood", "feFuncA", "feFuncB", "feFuncG", "feFuncR", "feGaussianBlur", "feImage", "feMerge", "feMergeNode",
            "feMorphology", "feOffset", "fePointLight", "feSpecularLighting", "feSpotLight", "feTile", "feTurbulence",
            "fieldset", "figcaption", "figure", "filter", "font", "footer", "foreignObject", "form", "frame", "frameset",
            "g", "h1", "h2", "h3", "h4", "h5", "h6", "head", "header", "hgroup", "hr", "html", "i", "iframe", "image", "img",
            "input", "ins", "isindex", "kbd", "keygen", "label", "legend", "li", "line", "linearGradient", "link",
            "listing", "main", "map", "mark", "marker", "marquee", "mask", "menu", "meta", "metadata", "meter",
            "mpath", "multicol", "nav", "nextid", "nobr", "noscript", "object", "ol", "optgroup", "option", "output", "p",
            "param", "path", "pattern", "picture", "plaintext", "polygon", "polyline", "pre",
            "progress", "q", "radialGradient", "rb", "rect", "rp", "rt", "rtc", "ruby", "s", "samp", "script", "section",
            "select", "set", "slot", "small", "source", "spacer", "span", "stop", "strike", "strong", "style",
            "sub", "summary", "sup", "svg", "switch", "symbol", "table", "tbody", "td", "template", "text", "textPath",
            "textarea", "tfoot", "th", "thead", "time", "title", "tr", "track", "tspan", "tt", "u", "ul",
            "unknown", "use", "var", "video", "wbr", "xmp"
          ];
          // 有特殊标签的网页例如 vue.js 会有组件标签，考虑不替换资源路径
          function hasUnknownTags() {
            const tags = document.getElementsByTagName('*');
            for (let i = 0; i < tags.length; i++)
              // 如果节点名称不在HTML标准标签列表中，则表示存在浏览器不识别的标签
              if (!validHTMLTags.includes(tags[i].nodeName.toLowerCase()))
                return true;
          }
          if (replace != 'none' && hasUnknownTags())
            replace = 'none'
          function full2locale(tag, attr, callback) {
            const aa = document.querySelectorAll(`${tag}[${attr}]`);
            for (const a of aa) {
              // a['href'] 和 a.href 都会变为全连接
              // a.getAttribute('href') 才能真正获得短链接
              const href = a.getAttribute(attr);
              if (!href || href == '#' || href.startsWith("javascript:"))
                continue;
              const url = new URL(a[attr]);
              if (replace != 'none') {
                if (href.startsWith("http://") || href.startsWith("https://")) {
                  // 全路径改为相对路径，让镜像下来的网站本站内跳转而不是跳回原站
                  if (replace == 'all' || url.host == window.location.host) {
                    if (url.host == window.location.host)
                      a.setAttribute(attr, url.pathname);
                    else
                      // 外站要包含外站的 host
                      a.setAttribute(attr, href.substring(url.protocol.length + 1));
                    rewrite = true;
                  }
                } else {
                  // 相对路径
                }
              }
              if (callback)
                callback(url, url.host == window.location.host ? 0 : -1);
            }
          }
          full2locale('a', 'href', (url, outside) => {
            // 去除 hash 和 参数
            const href = url.origin + url.pathname;
            if (hrefs[href] != undefined)
              return;
            hrefs[href] = outside;
          })
          // form
          full2locale('form', 'action');
          // link
          full2locale(':not(a)', 'href');
          // script | img | iframe | video
          full2locale('', 'src');
          // todo: 而且有些写在 css 或者 style 里的背景图片的引用路径修改不了
          return {
            url: window.location.href,
            html: rewrite ? ("<!DOCTYPE html>" + document.documentElement.outerHTML) : undefined,
            hrefs: hrefs,
          }
        }, (types || source) ? 'none' : (remote ? 'all' : 'local')).catch(e => {
          console.log("evaluate error", e)
        })
      if (html) {
        console.log("Sniffing results!")
        if (currentURL == html.url) {
          // 追加要跳转的链接
          for (const href in html.hrefs) {
            if (linkValue(href) != undefined)
              continue;
            linkValue(href, 0);
          }
          // 将相对路径的页面代码覆盖到文件
          // 替换了资源加载都会变成本站，镜像站点来说比较合理
          // 但是使用了 vue.js 等框架时，源码和解析出来的DOM并不一样
          // 一般来说，资源都是站内的相对路径
          // 源码：防止 vue.js 等特殊标签，可以做个检测 DOM 元素有没有特殊标签；缺点还需要手动替换所有资源路径
          // DOM: 替换了资源相对路径镜像站点直接可用，还相当于完成了预渲染；缺点还需要手动替换回源码
          if (html.html) {
            writeFile(urlp.parse(html.url, true), 'document', html.html)
            console.log("Modifying web page links to relative paths!", currentURL);
          }
        } else {
          console.log("URL redirected!", currentURL, "->", html.url)
        }
        linkValue(currentURL, 1);
        fs.writeFileSync(record, JSON.stringify(save, "", "  "));
        console.log("Url completed!", currentURL)
      }
    } else {
      // 稍微等待，免得切换页面太过于频繁
      await new Promise(r => { setTimeout(() => r(0), 1000) })
    }

    if (auto)
      automatic();
    else
      page.once('framenavigated', urlchanged);
  }
  automatic();

  process.stdin.on('data', input => {
    input = input.toString().trim();
    if (input == 'auto') {
      if (!auto) {
        auto = true;
        automatic();
        console.log("automatic operation!")
      } else
        console.log("in automatic operation")
    } else if (input == 'stop') {
      auto = false;
      console.log("stop automatic operation!")
    } else if (input == 'info') {
      information();
    }
  })
})()